﻿/*
* ----------------------------- JSTORAGE -------------------------------------
* Simple local storage wrapper to save data on the browser side, supporting
* all major browsers - IE6+, Firefox2+, Safari4+, Chrome4+ and Opera 10.5+
*
* Copyright (c) 2010 Andris Reinman, andris.reinman@gmail.com
* Project homepage: www.jstorage.info
*
* Licensed under MIT-style license:
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

/**
* $.jStorage
*
* USAGE:
*
* jStorage requires Prototype, MooTools or jQuery! If jQuery is used, then
* jQuery-JSON (http://code.google.com/p/jquery-json/) is also needed.
* (jQuery-JSON needs to be loaded BEFORE jStorage!)
*
* Methods:
*
* -set(key, value)
* $.jStorage.set(key, value) -> saves a value
*
* -get(key[, default])
* value = $.jStorage.get(key [, default]) ->
*    retrieves value if key exists, or default if it doesn't
*
* -deleteKey(key)
* $.jStorage.deleteKey(key) -> removes a key from the storage
*
* -flush()
* $.jStorage.flush() -> clears the cache
*
* -storageObj()
* $.jStorage.storageObj() -> returns a read-ony copy of the actual storage
*
* -storageSize()
* $.jStorage.storageSize() -> returns the size of the storage in bytes
*
* -index()
* $.jStorage.index() -> returns the used keys as an array
*
* -storageAvailable()
* $.jStorage.storageAvailable() -> returns true if storage is available
*
* -reInit()
* $.jStorage.reInit() -> reloads the data from browser storage
*
* <value> can be any JSON-able value, including objects and arrays.
*
**/

(function ($) {
	if (!$ || !($.toJSON || Object.toJSON || window.JSON)) {
		throw new Error("jQuery, MooTools or Prototype needs to be loaded before jStorage!");
	}

	var 
	/* This is the object, that holds the cached values */
        _storage = {},

	/* Actual browser storage (localStorage or globalStorage['domain']) */
        _storage_service = { jStorage: "{}" },

	/* DOM element for older IE versions, holds userData behavior */
        _storage_elm = null,

	/* How much space does the storage take */
        _storage_size = 0,

	/* function to encode objects to JSON strings */
        json_encode = $.toJSON || Object.toJSON || (window.JSON && (JSON.encode || JSON.stringify)),

	/* function to decode objects from JSON strings */
        json_decode = $.evalJSON || (window.JSON && (JSON.decode || JSON.parse)) || function (str) {
        	return String(str).evalJSON();
        },

	/* which backend is currently used */
        _backend = false,

	/* Next check for TTL */
        _ttl_timeout,

	/**
	* XML encoding and decoding as XML nodes can't be JSON'ized
	* XML nodes are encoded and decoded if the node is the value to be saved
	* but not if it's as a property of another object
	* Eg. -
	*   $.jStorage.set("key", xmlNode);        // IS OK
	*   $.jStorage.set("key", {xml: xmlNode}); // NOT OK
	*/
        _XMLService = {

        	/**
        	* Validates a XML node to be XML
        	* based on jQuery.isXML function
        	*/
        	isXML: function (elm) {
        		var documentElement = (elm ? elm.ownerDocument || elm : 0).documentElement;
        		return documentElement ? documentElement.nodeName !== "HTML" : false;
        	},

        	/**
        	* Encodes a XML node to string
        	* based on http://www.mercurytide.co.uk/news/article/issues-when-working-ajax/
        	*/
        	encode: function (xmlNode) {
        		if (!this.isXML(xmlNode)) {
        			return false;
        		}
        		try { // Mozilla, Webkit, Opera
        			return new XMLSerializer().serializeToString(xmlNode);
        		} catch (E1) {
        			try {  // IE
        				return xmlNode.xml;
        			} catch (E2) { }
        		}
        		return false;
        	},

        	/**
        	* Decodes a XML node from string
        	* loosely based on http://outwestmedia.com/jquery-plugins/xmldom/
        	*/
        	decode: function (xmlString) {
        		var dom_parser = ("DOMParser" in window && (new DOMParser()).parseFromString) ||
                        (window.ActiveXObject && function (_xmlString) {
                        	var xml_doc = new ActiveXObject('Microsoft.XMLDOM');
                        	xml_doc.async = 'false';
                        	xml_doc.loadXML(_xmlString);
                        	return xml_doc;
                        }),
                resultXML;
        		if (!dom_parser) {
        			return false;
        		}
        		resultXML = dom_parser.call("DOMParser" in window && (new DOMParser()) || window, xmlString, 'text/xml');
        		return this.isXML(resultXML) ? resultXML : false;
        	}
        };

	////////////////////////// PRIVATE METHODS ////////////////////////

	/**
	* Initialization function. Detects if the browser supports DOM Storage
	* or userData behavior and behaves accordingly.
	* @returns undefined
	*/
	function _init() {
		/* Check if browser supports localStorage */
		var localStorageReallyWorks = false;
		if ("localStorage" in window) {
			try {
				window.localStorage.setItem('_tmptest', 'tmpval');
				localStorageReallyWorks = true;
				window.localStorage.removeItem('_tmptest');
			} catch (BogusQuotaExceededErrorOnIos5) {
				// Thanks be to iOS5 Private Browsing mode which throws
				// QUOTA_EXCEEDED_ERRROR DOM Exception 22.
			}
		}
		if (localStorageReallyWorks) {
			try {
				if (window.localStorage) {
					_storage_service = window.localStorage;
					_backend = "localStorage";
				}
			} catch (E3) { /* Firefox fails when touching localStorage and cookies are disabled */ }
		}
		/* Check if browser supports globalStorage */
		else if ("globalStorage" in window) {
			try {
				if (window.globalStorage) {
					_storage_service = window.globalStorage[window.location.hostname];
					_backend = "globalStorage";
				}
			} catch (E4) { /* Firefox fails when touching localStorage and cookies are disabled */ }
		}
		/* Check if browser supports userData behavior */
		else {
			_storage_elm = document.createElement('link');
			if (_storage_elm.addBehavior) {

				/* Use a DOM element to act as userData storage */
				_storage_elm.style.behavior = 'url(#default#userData)';

				/* userData element needs to be inserted into the DOM! */
				document.getElementsByTagName('head')[0].appendChild(_storage_elm);

				_storage_elm.load("jStorage");
				var data = "{}";
				try {
					data = _storage_elm.getAttribute("jStorage");
				} catch (E5) { }
				_storage_service.jStorage = data;
				_backend = "userDataBehavior";
			} else {
				_storage_elm = null;
				return;
			}
		}

		_load_storage();

		// remove dead keys
		_handleTTL();
	}

	/**
	* Loads the data from the storage based on the supported mechanism
	* @returns undefined
	*/
	function _load_storage() {
		/* if jStorage string is retrieved, then decode it */
		if (_storage_service.jStorage) {
			try {
				_storage = json_decode(String(_storage_service.jStorage));
			} catch (E6) { _storage_service.jStorage = "{}"; }
		} else {
			_storage_service.jStorage = "{}";
		}
		_storage_size = _storage_service.jStorage ? String(_storage_service.jStorage).length : 0;
	}

	/**
	* This functions provides the "save" mechanism to store the jStorage object
	* @returns undefined
	*/
	function _save() {
		try {
			_storage_service.jStorage = json_encode(_storage);
			// If userData is used as the storage engine, additional
			if (_storage_elm) {
				_storage_elm.setAttribute("jStorage", _storage_service.jStorage);
				_storage_elm.save("jStorage");
			}
			_storage_size = _storage_service.jStorage ? String(_storage_service.jStorage).length : 0;
		} catch (E7) { /* probably cache is full, nothing is saved this way*/ }
	}

	/**
	* Function checks if a key is set and is string or numberic
	*/
	function _checkKey(key) {
		if (!key || (typeof key != "string" && typeof key != "number")) {
			throw new TypeError('Key name must be string or numeric');
		}
		if (key == "__jstorage_meta") {
			throw new TypeError('Reserved key name');
		}
		return true;
	}

	/**
	* Removes expired keys
	*/
	function _handleTTL() {
		var curtime, i, TTL, nextExpire = Infinity, changed = false;

		clearTimeout(_ttl_timeout);

		if (!_storage.__jstorage_meta || typeof _storage.__jstorage_meta.TTL != "object") {
			// nothing to do here
			return;
		}

		curtime = +new Date();
		TTL = _storage.__jstorage_meta.TTL;
		for (i in TTL) {
			if (TTL.hasOwnProperty(i)) {
				if (TTL[i] <= curtime) {
					delete TTL[i];
					delete _storage[i];
					changed = true;
				} else if (TTL[i] < nextExpire) {
					nextExpire = TTL[i];
				}
			}
		}

		// set next check
		if (nextExpire != Infinity) {
			_ttl_timeout = setTimeout(_handleTTL, nextExpire - curtime);
		}

		// save changes
		if (changed) {
			_save();
		}
	}

	////////////////////////// PUBLIC INTERFACE /////////////////////////

	$.jStorage = {
		/* Version number */
		version: "0.1.6.1",

		/**
		* Sets a key's value.
		*
		* @param {String} key - Key to set. If this value is not set or not
		*              a string an exception is raised.
		* @param value - Value to set. This can be any value that is JSON
		*              compatible (Numbers, Strings, Objects etc.).
		* @returns the used value
		*/
		set: function (key, value) {
			_checkKey(key);
			if (_XMLService.isXML(value)) {
				value = { _is_xml: true, xml: _XMLService.encode(value) };
			} else if (typeof value == "function") {
				value = null; // functions can't be saved!
			} else if (value && typeof value == "object") {
				// clone the object before saving to _storage tree
				value = json_decode(json_encode(value));
			}
			_storage[key] = value;
			_save();
			return value;
		},

		/**
		* Looks up a key in cache
		*
		* @param {String} key - Key to look up.
		* @param {mixed} def - Default value to return, if key didn't exist.
		* @returns the key value, default value or <null>
		*/
		get: function (key, def) {
			_checkKey(key);
			if (key in _storage) {
				if (_storage[key] && typeof _storage[key] == "object" &&
                        _storage[key]._is_xml &&
                            _storage[key]._is_xml) {
					return _XMLService.decode(_storage[key].xml);
				} else {
					return _storage[key];
				}
			}
			return typeof (def) == 'undefined' ? null : def;
		},

		/**
		* Deletes a key from cache.
		*
		* @param {String} key - Key to delete.
		* @returns true if key existed or false if it didn't
		*/
		deleteKey: function (key) {
			_checkKey(key);
			if (key in _storage) {
				delete _storage[key];
				// remove from TTL list
				if (_storage.__jstorage_meta &&
                  typeof _storage.__jstorage_meta.TTL == "object" &&
                  key in _storage.__jstorage_meta.TTL) {
					delete _storage.__jstorage_meta.TTL[key];
				}
				_save();
				return true;
			}
			return false;
		},

		/**
		* Sets a TTL for a key, or remove it if ttl value is 0 or below
		*
		* @param {String} key - key to set the TTL for
		* @param {Number} ttl - TTL timeout in milliseconds
		* @returns true if key existed or false if it didn't
		*/
		setTTL: function (key, ttl) {
			var curtime = +new Date();
			_checkKey(key);
			ttl = Number(ttl) || 0;
			if (key in _storage) {

				if (!_storage.__jstorage_meta) {
					_storage.__jstorage_meta = {};
				}
				if (!_storage.__jstorage_meta.TTL) {
					_storage.__jstorage_meta.TTL = {};
				}

				// Set TTL value for the key
				if (ttl > 0) {
					_storage.__jstorage_meta.TTL[key] = curtime + ttl;
				} else {
					delete _storage.__jstorage_meta.TTL[key];
				}

				_save();

				_handleTTL();
				return true;
			}
			return false;
		},

		/**
		* Deletes everything in cache.
		*
		* @return true
		*/
		flush: function () {
			_storage = {};
			_save();
			return true;
		},

		/**
		* Returns a read-only copy of _storage
		*
		* @returns Object
		*/
		storageObj: function () {
			function F() { }
			F.prototype = _storage;
			return new F();
		},

		/**
		* Returns an index of all used keys as an array
		* ['key1', 'key2',..'keyN']
		*
		* @returns Array
		*/
		index: function () {
			var index = [], i;
			for (i in _storage) {
				if (_storage.hasOwnProperty(i) && i != "__jstorage_meta") {
					index.push(i);
				}
			}
			return index;
		},

		/**
		* How much space in bytes does the storage take?
		*
		* @returns Number
		*/
		storageSize: function () {
			return _storage_size;
		},

		/**
		* Which backend is currently in use?
		*
		* @returns String
		*/
		currentBackend: function () {
			return _backend;
		},

		/**
		* Test if storage is available
		*
		* @returns Boolean
		*/
		storageAvailable: function () {
			return !!_backend;
		},

		/**
		* Reloads the data from browser storage
		*
		* @returns undefined
		*/
		reInit: function () {
			var new_storage_elm, data;
			if (_storage_elm && _storage_elm.addBehavior) {
				new_storage_elm = document.createElement('link');

				_storage_elm.parentNode.replaceChild(new_storage_elm, _storage_elm);
				_storage_elm = new_storage_elm;

				/* Use a DOM element to act as userData storage */
				_storage_elm.style.behavior = 'url(#default#userData)';

				/* userData element needs to be inserted into the DOM! */
				document.getElementsByTagName('head')[0].appendChild(_storage_elm);

				_storage_elm.load("jStorage");
				data = "{}";
				try {
					data = _storage_elm.getAttribute("jStorage");
				} catch (E5) { }
				_storage_service.jStorage = data;
				_backend = "userDataBehavior";
			}

			_load_storage();
		}
	};

	// Initialize jStorage
	_init();

})(window.jQuery || window.$);